import pyglet
from pyglet.window import key
from pyglet.gl import *
import math
import copy
import time
from itertools import chain

PI = 3.14159

window = pyglet.window.Window()
keys = key.KeyStateHandler()
window.push_handlers(keys)

class Vector(object):
  def __init__(self, x=0, y=0):
    self.x = x
    self.y = y

  def __str__(self):
    return "(x=%f, y=%f)" % (self.x, self.y)

  def __add__(self, other):
    return Vector(self.x + other.x, self.y + other.y)
  
  def __sub__(self, other):
    return Vector(self.x - other.x, self.y - other.y)

  def __iadd__(self, other):
    self.x += other.x
    self.y += other.y
    return self

  def __isub__(self, other):
    self.x -= other.x
    self.y -= other.y
    return self
  
  def normalize(self):
    mag = self.mag()
    return Vector(self.x / mag, self.y / mag)

  def __mul__(self, other):
    return Vector(self.x * other.x, self.y * other.y)
  
  def smul(self, scalar):
    return Vector(self.x * scalar, self.y * scalar)

  def dp(self, other):
    return self.x*other.x + self.y*other.y

  def rnorm(self):
    return Vector(-self.y, self.x)

  def lnorm(self):
    return Vector(self.y, -self.x)

  def magSquared(self):
    return abs(self.x*self.x + self.y*self.y)

  def mag(self):
    return math.sqrt(self.x*self.x + self.y*self.y)

  def projection(self, other):
    dp = self.dp(other)
    dpOverOtherMag2 = dp / (other.x*other.x + other.y*other.y)
    return Vector(dpOverOtherMag2 * other.x,
                  dpOverOtherMag2 * other.y)

  def addPolar(self, angle, magnitude):
    return Vector(self.x + math.cos(angle) * magnitude,
                  self.y + math.sin(angle) * magnitude)

  def distance(self, other):
    return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)

  def angle(self):
    return math.atan2(self.y, self.x)

  def relAngle(self, other):
    return (self - other).angle()

def polarVector(angle, magnitude):
  return Vector(math.cos(angle) * magnitude, math.sin(angle) * magnitude)

def pairs(lst):
  i = iter(lst)
  first = prev = item = i.next()
  for item in i:
    yield prev, item
    prev = item
  yield item, first

def triples(lst):
  i = iter(lst)
  first = prev2 = i.next()
  second = prev1 = item = i.next()
  for item in i:
    yield prev2, prev1, item
    prev2 = prev1
    prev1 = item
  yield prev2, item, first
  yield item, first, second

class Objects(object):
  def __init__(self):
    self.universe = set()
    self.humans = set()
    self.computers = set()
    self.tanks = set()
    self.obstacles = set()
    self.bullets = set()
    """self.walls = Walls()
    self.universe.add(self.walls)"""
    self.trash = set()

  def addHuman(self, item):
    self.humans.add(item)
    self.tanks.add(item)
    self.universe.add(item)   

  def addComputer(self, item):
    self.computers.add(item)
    self.tanks.add(item)
    self.universe.add(item)   
  
  def addObstacle(self, item):
    self.obstacles.add(item)
    self.universe.add(item)   

  def addBullet(self, item):
    self.bullets.add(item)
    self.universe.add(item)   

  def trashObject(self, item):
    self.trash.add(item)

  def clean(self):
    while self.trash:
      item = self.trash.pop()
      self.universe.discard(item)
      self.humans.discard(item)
      self.tanks.discard(item)
      self.obstacles.discard(item)
      self.bullets.discard(item)

class Walls(object):
  def __init__(self):
    self.parent = None

  def render(self):
    pass  

  def getCollision(self, circle):
    if not isinstance(circle, Circle): return None
    if (circle.center.x < circle.radius or
        circle.center.x > window.width - circle.radius or
        circle.center.y < circle.radius or
        circle.center.y > window.height - circle.radius):
      newCenter = copy.copy(circle.center)
      newCenter.x = max(newCenter.x, circle.radius)
      newCenter.x = min(newCenter.x, window.width - circle.radius)
      newCenter.y = max(newCenter.y, circle.radius)
      newCenter.y = min(newCenter.y, window.height - circle.radius)
      return newCenter - circle.center
    return None  


class Shape(object):
  def __init__(self, parent, center, facing):
    self._parent = parent
    self._center = center
    self._velocity = Vector()
    self._angular_v = 0
    self._facing = 0
    self._speed = 0
    self.shapes = [] 

  @property
  def parent(self):
    return self._parent if self._parent else self

  @property
  def center(self):
    return self._parent.center if self._parent else self._center

  @center.setter
  def center(self, vector):
    if self._parent:
      self._parent.center = vector
    else:
      self._center = vector

  @property
  def velocity(self):
    return self._parent.velocity if self._parent else self._velocity

  @velocity.setter
  def velocity(self, vector):
    if self._parent:
      self._parent.velocity = vector
    else:
      self._velocity = vector

  @property
  def angular_v(self):
    return self._parent.angular_v if self._parent else self._angular_v

  @angular_v.setter
  def angular_v(self, scalar):
    if self._parent:
      self._parent.angular_v = scalar
    else:
      self._angular_v = scalar

  @property
  def facing(self):
    return self._parent.facing if self._parent else self._facing

  @facing.setter
  def facing(self, angle):
    if self._parent:
      self._parent.facing = angle
    else:
      self._facing = angle

  @property
  def speed(self):
    return self._parent.speed if self._parent else self._speed

  @speed.setter
  def speed(self, scalar):
    if self._parent:
      self._parent.speed = scalar
    else:
      self._speed = scalar

  def _updatePosition(self):
    pass
 
  def _updateVelocity(self):
    self._velocity = polarVector(self.facing, self.speed)
 
  def render(self):
    pass

  def getCollision(self):
    return None

  def move(self):
    oldCenter = copy.copy(self.center)
    self.center += self.velocity
    self.facing = (self.facing + self.angular_v) % (2*PI)
    self._updatePosition()
    objectHit = None
    displacement = None
    for item in objects.universe:
      displacement = self.getCollision(item)
      if displacement:
        objectHit = item
        break
    displacement = displacement.smul(-1) if displacement else None
    self.post_move(objectHit, displacement, oldCenter)
    self._updatePosition()
  
  def post_move(self, objectHit, displacement, oldCenter):
    if objectHit:
      self.center = self.center + displacement
      self.velocity = self.center - oldCenter
  
  def rotate(self, angular_v):
    self.angular_v = angular_v

class SimpleShape(Shape):
  def __init__(self, parent, center, facing, vertices): 
    super(SimpleShape, self).__init__(parent, center, facing)
    self.polar_vertices = [(math.atan2(v.y, v.x), v.mag()) for v in vertices]
    self.color = [0, 0, 0]
    self.shapes = [self]
    self._updatePosition()
    self.vertex_list = pyglet.graphics.vertex_list(
        len(self.vertices), 'v2d', 'c3B')
   
  def _updatePosition(self):
    self.vertices = [self.center.addPolar(a + self.facing, m)
        for (a, m) in self.polar_vertices]
  
  def render(self):
    output = list(chain.from_iterable((v.x, v.y) for v in self.vertices))
    self.vertex_list.vertices = output
    self.vertex_list.draw(pyglet.gl.GL_POLYGON)
    colorList = [self.color for v in self.vertices]
    colorList = [item for sublist in colorList for item in sublist]
    self.vertex_list.colors = colorList

    
class Polygon(SimpleShape):
  def __init__(self, parent, center, vertices):
    super(Polygon, self).__init__(parent, center, 0, vertices)


  def getCollision(self, shape):
    if self.parent == shape.parent: return None
    dispSum = Vector()
    collision = False
    for sub_item in shape.shapes:
      if isinstance(shape, Circle):
        disp = self.getCollisionWithCircle(sub_item)
      if isinstance(shape, Polygon):
        disp = self.getCollisionWithPolygon(sub_item)
      if disp:
        dispSum += disp
        collision = True
    return dispSum if collision else None

  def boundaryDpAlongVector(self, vector):
    min_dp = max_dp = self.vertices[0].dp(vector)
    for v in self.vertices:
      dp = v.dp(vector)
      if dp < min_dp:
        min_dp = dp
      elif dp > max_dp:
        max_dp = dp
    return (min_dp, max_dp)


  def getMinOverlap(self, polygon, axes):
    min_overlap = 999999.0
    direction = Vector()
    for (a, b) in axes:
      axis = (b - a).rnorm()
      (self_min, self_max) = self.boundaryDpAlongVector(axis)
      (other_min, other_max) = polygon.boundaryDpAlongVector(axis)
      if other_min > self_max or other_max < self_min:
        return None 
      mag = axis.mag()
      overlap = min(self_max - other_min, other_max - self_min)
      overlap = overlap / mag
      if overlap < min_overlap:
        min_overlap = overlap
        direction = axis.smul(overlap/mag)
    return (min_overlap, direction)

  def getCollisionWithPolygon(self, polygon):
    overlap = self.getMinOverlap(polygon, pairs(self.vertices))
    if not overlap:
      return None

    other_overlap = self.getMinOverlap(polygon, pairs(polygon.vertices))
    if not other_overlap:
      return None

    (self_overlap, self_direction) = overlap
    (other_overlap, other_direction) = other_overlap
    self_direction = self_direction.smul(-1)
    if self_overlap < other_overlap:
      return self_direction
    else:
      return other_direction

  def getCollisionWithCircle(self, circle):
    p = circle.center
    point = None
    for (a, b) in pairs(self.vertices):
      faceVector = b - a
      pointVector = p - a
      if (faceVector).rnorm().dp(pointVector) > 0:
        continue
      pp = a + pointVector.projection(faceVector)
      if (pp - a).dp(pp - b) < 0:
        point = pp
        break
    if not point:
      for (a, b, c) in triples(self.vertices):
        pv = p - b
        if (b - a).rnorm().dp(pv) < 0 and (b - c).lnorm().dp(pv) < 0:
          point = b
          break
    if point:
      distSquared = (point - p).magSquared() 
      if distSquared < circle.radius*circle.radius:
        overlap = math.sqrt(distSquared) - circle.radius
        return (point - p).normalize().smul(overlap)
    return None


class Circle(SimpleShape):
  def __init__(self, parent, center, radius):
    self.radius = radius
    rel_vertices = []
    segments = int(max(4, self.radius * 0.4))
    angle = 0
    while angle < 2 * PI:
      rel_vertices.append(Vector(math.cos(angle) * radius,
                                 math.sin(angle) * radius))
      angle += PI / segments
    super(Circle, self).__init__(parent, center, 0, rel_vertices)

  def getCollision(self, shape):
    if self.parent == shape.parent: return None
    dispSum = Vector()
    collision = False
    for sub_item in shape.shapes:
      if isinstance(shape, Circle):
        disp = self.getCollisionWithCircle(sub_item)
      if isinstance(shape, Polygon):
        disp = shape.getCollisionWithCircle(self)
        disp = disp.smul(-1) if disp else None
      if disp:
        dispSum += disp
        collision = True
    return dispSum if collision else None

  def getCollisionWithCircle(self, circle):  
    distanceSquared = (self.center - circle.center).magSquared()
    radSum = self.radius + circle.radius
    if (distanceSquared < radSum*radSum):
       return (circle.center - self.center).normalize().smul(
           radSum - math.sqrt(distanceSquared))
    return None


class Bullet(Circle):
  def __init__(self, tank):
    super(Bullet, self).__init__(None,
        copy.copy(tank.center.addPolar(tank.facing, tank.gun_length)), 3)
    self.velocity = polarVector(tank.facing, tank.bullet_speed)

  def post_move(self, objectHit, displacement, oldCenter):
    if not objectHit: return
    if objectHit in objects.tanks:
      objectHit.hit(self)
    objects.trashObject(self)

class CompositeShape(Shape):
  def __init__(self, parent, center, facing):
    super(CompositeShape, self).__init__(parent, center, facing)

  def addShape(self, shape):
    shape._parent = self
    self.shapes.append(shape)

  def _updatePosition(self):
    for shape in self.shapes:
      shape._updatePosition()

  def render(self):
    for shape in self.shapes:
      shape.render()

  def getCollision(self, item):
    dispSum = Vector()
    collision = False
    for shape in self.shapes:
      for sub_item in item.shapes:
        disp = shape.getCollision(sub_item)
        if disp:
          dispSum += disp
          collision = True
    return dispSum if collision else None

class Tank(CompositeShape):
  def __init__(self, center, radius, facing):
    self.max_speed = 2
    self.bullet_speed = 4
    self.last_shot = 0
    self.radius = radius
    self.gun_length = radius * 1.5
    super(Tank, self).__init__(None, center, facing)
    self.circle = Circle(self, center, radius)
    self.circle.color = [0,0,255]
    self.addShape(self.circle)
    self.addShape(Polygon(self, center,
        [Vector(0, -2), Vector(self.gun_length, -2),
         Vector(self.gun_length, 2), Vector(0, 2)]))

  def drive(self, direction):
    self.speed = self.max_speed * direction
    self._updateVelocity()
  
  def checkKeys(self):
    if keys[key.RIGHT]:
      self.rotate(-.1)
    elif keys[key.LEFT]:
      self.rotate(.1)
    else:
      self.rotate(0)

    if keys[key.UP]:
      self.drive(1)
    elif keys[key.DOWN]:
      self.drive(-1)
    else:
      self.drive(0)

    self.move()
    if keys[key.SPACE]:
      self.shoot()
  
  def shoot(self):
    if self.last_shot + 1 < time.time():
      objects.addBullet(Bullet(self))
      self.last_shot = time.time()

  def hit(self, bullet):
    self.circle.color[2] -= 50

objects = Objects()
objects.addObstacle(Circle(None, Vector(300,300), 50))
objects.addObstacle(Polygon(None, Vector(300, 100),
                            [Vector(0,-30), Vector(30,-10), Vector(20,10),
                             Vector(-10,20), Vector(-20,-20)]))
objects.addHuman(Tank(Vector(100,100), 20, 0))
objects.addComputer(Tank(Vector(500, 200), 20, 0))

fps_display = pyglet.clock.ClockDisplay()

@window.event
def on_draw():  
  glClearColor(.8, .8, .8, 1)
  glClear(GL_COLOR_BUFFER_BIT)
  glLoadIdentity()
  for item in objects.universe:
    item.render()
  fps_display.draw()

def update(dt):
  for human in objects.humans:
    human.checkKeys()
 
  for bullet in objects.bullets:
    bullet.move()

  objects.clean()

pyglet.clock.schedule_interval(update, 1/60.0)
glEnable(GL_LINE_SMOOTH)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE)
pyglet.app.run()
